package tv.geir.slackspace.matching;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import net.jini.id.Uuid;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import tv.geir.slackspace.common.SlackSpaceException;
import tv.geir.slackspace.common.UuidGenerator;

/**
 * A template is used to model an object, that being the object's:
 * <ul>
 * 	 <li>Class name</li>
 * 	 <li>Superclasses (names)</li>
 * 	 <li>Implemented interfaces (names) </li>
 * 	 <li>Public fields (names, types and values) </li>
 *   <li>Bean properties (names, types and values)</li>
 * </ul>
 * Templates can be created from other objects. In the following example a template
 * is created by passing a reference to another object to <code>Template</code>'s 
 * constructor. This will trigger an analyzis process which will identify <code>myObject</code>'s
 * class name, superclasses, interfaces, and so on.. 
 * <pre>
 * // create a template that models myObject
 * Template exactMatchingTemplate = new Template( myObject );
 * </pre>
 * Templates can also be created manually:
 * <pre>
 * Template myTemplate = new Template();
 * myTemplate.add( )
 * </pre>
 * @author Geir Gullestad Pettersen
 * @see tv.geir.slackspace.matching.TemplateMatcher
 *
 */
public class Template implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private Set<String> interfaces;

	private Set<String> superClasses;

	private Set<TemplateProperty> beanProperties;
	
	private Set<TemplateProperty> publicFields;

	private String name;

	private String packageName;

	private Uuid id;

	/**
	 * When set to true all properties will be created with <code>primitiveDefaultValueWildcard</code>
	 * set to <code>true</code>
	 */
	private boolean primitiveDefaultValueWildcard;
	
	private boolean forSpecificInstance;
	
	private List<Throwable> errors;

	private transient static Log log = LogFactory.getLog(Template.class);

	private transient static UuidGenerator uuidGenerator = new UuidGenerator(); 

	/**
	 * Constructs a Template. 
	 */
	public Template() {
		interfaces = new HashSet<String>();
		superClasses = new LinkedHashSet<String>();
		beanProperties = new LinkedHashSet<TemplateProperty>();
		publicFields = new LinkedHashSet<TemplateProperty>();
		errors = new ArrayList<Throwable>();
		primitiveDefaultValueWildcard = true;
		
	}

	/** 
	 * Constructs a template representing <code>o</code>. 
	 * @param o
	 * @param forSpecificInstance sets this to true if this is
	 * 		 	template is representing a specific instance. Set 
	 * 		    to false if it is representing any instance matching
	 * 		    other template properties( public fields, 
	 * 			javabean properties, classname, 
	 * 			superclasses, interfaces  
	 */
	public Template(Object o, boolean forSpecificInstance) {
		this();
		this.forSpecificInstance = forSpecificInstance;
		setFromObject(o);		
	
	}

	public void setFromObject(Object o) {
		Class c = o.getClass();
		// set uuid if this is not a search 		
		id = uuidGenerator.newUuid();
		
		// class name
		setName(c.getName());
		if( log.isDebugEnabled() ) {
			log.debug("Analyzing " + getName());
		}
		
		// superclasses and interfaces
		// TODO This should be cached
		addSuperClassesAndInterfaces(o.getClass());

			
		// public fields
		publicFields = getPublicFields( o );
		
		// properties			
		beanProperties = getBeanProperties(o);

		
		
	}

	/**
	 * Gets all public fields from <code>o</code>.
	 * @param o
	 */
	private Set<TemplateProperty> getPublicFields( Object o) {
		Field[] fields = o.getClass().getFields();
		Set<TemplateProperty> pFields = new HashSet<TemplateProperty>();
		for( Field field : fields ) {
			TemplateProperty f = new TemplateProperty();
			f.name = field.getName();
			f.type = field.getType().getName();
			f.setDefaultPrimitiveIsWildcard( primitiveDefaultValueWildcard );
			try {
				f.setValue( field.get( o ));
			} catch (Throwable t) {
				log.warn( t );
				errors.add( new SlackSpaceException(
						"Error accessing public field \'" + 
						f.name + "\': " + t.getMessage(), t ));
			}
			pFields.add( f );
		}
		return pFields;		
	}
	
	/**
	 * Adds all superclasses and interfaces from <code>c</code>.
	 * @param c
	 */
	private void addSuperClassesAndInterfaces(Class c) {
		Class mother = c.getSuperclass();

		for (Class member : c.getInterfaces()) {
			String name = member.getName();
			interfaces.add(name);
			if( log.isDebugEnabled()) {
				log.debug("Interface: " + name);
			}

		}

		if (mother != null) {
			String name = mother.getName();
			superClasses.add(name);
			if( log.isDebugEnabled() ) {
				log.debug("Superclass: " + name);
			}
			addSuperClassesAndInterfaces(mother);
		}

	}

	/**
	 * Gets all properties from <code>o</code>
	 * @param o the object to extract properties from
	 * @param pds array of properties to extract
	 * @return properties found in <code>o</code>.	 
	 */
	private Set<TemplateProperty> getBeanProperties(Object o) {
		
		Set<TemplateProperty> properties = new HashSet<TemplateProperty>();
		try {
			// get list of properties from Introspector
			BeanInfo bi;
			bi = Introspector.getBeanInfo(o.getClass());		
			PropertyDescriptor[] pds = bi.getPropertyDescriptors();
			if( log.isDebugEnabled() ) {
				log.debug("Properties: " + pds);
			}
			
			/* loop through all properties descriptors and invoke 
			 * read-method on the object to extract value.
			 */
			for (PropertyDescriptor property : pds) {
				TemplateProperty p = new TemplateProperty();
				p.name = property.getDisplayName();
				p.type = property.getPropertyType().getName();
				p.setDefaultPrimitiveIsWildcard( primitiveDefaultValueWildcard );
				Method readMethod = property.getReadMethod();
				
				// can only access the property if there is a read method
				if (readMethod != null) { 
					if( log.isDebugEnabled() ) {
						log.debug("Fetching value using getter: " + readMethod);
					}
					// use get method
					try {
						p.setValue( readMethod.invoke( o ));
					} catch (Exception e) {
						log.warn("Unable to read property", e);
					}
					if( log.isDebugEnabled() ) {
						log.debug("Found property: " + p);
					}
					properties.add(p);
				} else {
					if( log.isDebugEnabled() ) {
						log.debug("Property " + p.name + " has no read method");
					}
	
				}
			}
			return properties;
		} catch (IntrospectionException ie) {
			throw new TemplateCreationException("Error extracting bean properties", ie );
		}
	}

	
	/**
	 * Adds a bean property to this template
	 * @param propertyName
	 * @param propertyType
	 * @param value
	 */
	public void addBeanProperty( String propertyName, String propertyType, Object value ) {
		addBeanProperty( new TemplateProperty( propertyName, propertyType, value ));
	}
		
	/**
	 * Adds a bean property to this template
	 * @param p
	 */
	public void addBeanProperty( TemplateProperty p ) {
		beanProperties.add( p );
	}
	
	/**
	 * Adds a public property to this template
	 * @param fieldName
	 * @param fieldType
	 * @param value
	 */
	public void addPublicField( String fieldName, String fieldType, Object value ) {
		addPublicField( new TemplateProperty( fieldName, fieldType, value ));
	}
	
	/**
	 * Adds a public field to this template
	 * @param f
	 */
	public void addPublicField( TemplateProperty f ) {
		publicFields.add( f );
	}
	/**
	 * Returns true if the class this template is representing has a 
	 * superclass of type <code>Class.forname( name )</code>
	 * @param name name of the class
	 * @return
	 */
	public boolean hasSuperClass( String canonicalName ) {
		for( String s : superClasses ) {
			if( s.equals( canonicalName )) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns true if the class this template is representing implements
	 * the interface of type <code>Class.forname( name )</code>
	 * @param name name of the interface
	 * @return
	 */
	public boolean hasInterface( String canonicalName ) {
		for( String i : interfaces ) {
			if( i.equals( canonicalName )) {
				return true;
			}
		}
		return false;
	}
	
	private TemplateProperty getTemplateProperty( String propertyName, Set<TemplateProperty> fromProperties ) {
		for( TemplateProperty p : fromProperties ) {
			if( propertyName.equals( p.name )) {
				return p;
			}
		}
		return null;
	}
	
	/**
	 * 
	 * @param properties
	 * @param flag
	 */
	private void setPrimitiveDefaultValueWildcard( Set<TemplateProperty> properties, boolean flag ) {
		for( TemplateProperty prop : properties ) {
			prop.setDefaultPrimitiveIsWildcard( flag );
		}
	}
	
	/**
	 * Returns a bean-property for this template. If no property is found the value
	 * <code>null</code> is returned.
	 * @param name
	 * @return
	 */
	public TemplateProperty getBeanProperty( String name ) {
		return getTemplateProperty( name, beanProperties );
	}
	
	/**
	 * Returns a public field for this template. If no property is found the value
	 * <code>null</code> is returned.
	 * @param name
	 * @return
	 */
	public TemplateProperty getPublicField( String name ) {
		return getTemplateProperty( name, publicFields );
	}
	
	public Set<String> getInterfaces() {
		return interfaces;
	}

	public void setInterfaces(Set<String> interfaces) {
		this.interfaces = interfaces;
	}

	public boolean isPrimitiveDefaultValueWildcard() {
		return primitiveDefaultValueWildcard;
	}

	/**
	 * Sets primitiveDefaultValueWildcard. All public fields and bean properties
	 * are updated with the same setting as well.
	 * 
	 * @param flag
	 */
	public void setPrimitiveDefaultValueWildcard(boolean flag) {
		this.primitiveDefaultValueWildcard = flag;
		// change all fields an properties
		setPrimitiveDefaultValueWildcard( beanProperties, flag );
		setPrimitiveDefaultValueWildcard( publicFields, flag );
	}

	
	public String getPackageName() {
		return packageName;
	}

	public void setPackageName(String packageName) {
		this.packageName = packageName;
	}

	public Set<TemplateProperty> getBeanProperties() {
		return beanProperties;
	}

	public void setBeanProperties(Set<TemplateProperty> beanProperties) {
		this.beanProperties = beanProperties;
	}

	public String getName() {
		return name;
	}

	public void setName(String canonicalName) {
		this.name = canonicalName;
	}

	public Set<String> getSuperClasses() {
		return superClasses;
	}

	public void setSuperClasses(Set<String> superClasses) {
		this.superClasses = superClasses;
	}

	public Uuid getId() {
		return id;
	}

	public void setId(Uuid id) {
		this.id = id;
		forSpecificInstance = true;
	}

	public boolean isForSpecificInstance() {
		return forSpecificInstance;
	}

	public void setForSpecificInstance(boolean forSpecificInstance) {
		this.forSpecificInstance = forSpecificInstance;
	}

	public Set<TemplateProperty> getPublicFields() {
		return publicFields;
	}

	public void setPublicFields(Set<TemplateProperty> fields) {
		this.publicFields = fields;
	}
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return new ToStringBuilder(this)
		.append("id", id)
		.append("className", name )
		.toString();
	}
}
